// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:io';

import 'package:front_end/src/api_unstable/vm.dart';
import 'package:front_end/src/api_prototype/constant_evaluator.dart'
    show SimpleErrorReporter;
import 'package:kernel/target/targets.dart';
import 'package:kernel/ast.dart';
import 'package:kernel/kernel.dart';
import 'package:kernel/verifier.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'package:vm/target_os.dart';
import 'package:vm/modular/target/vm.dart' show VmTarget;
import 'package:vm/transformations/unreachable_code_elimination.dart'
    show PlatformConstError, transformComponent;
import 'package:vm/transformations/vm_constant_evaluator.dart';

import '../common_test_utils.dart';

final String pkgVmDir = Platform.script.resolve('../..').toFilePath();

class TestErrorReporter extends SimpleErrorReporter {
  final reportedMessages = <String>[];

  TestErrorReporter();

  @override
  void reportMessage(Uri? uri, int offset, String message) {
    final buffer = StringBuffer();
    if (offset >= 0) {
      if (uri != null) {
        buffer
          ..write(uri.pathSegments.last)
          ..write(':');
      }
      buffer
        ..write(offset)
        ..write(' ');
    }
    buffer
      ..write('Constant evaluation error: ')
      ..write(message);
    reportedMessages.add(buffer.toString());
  }
}

class TestCase {
  final TargetOS os;
  final bool debug;
  final bool enableAsserts;
  final bool throws;

  const TestCase(
    this.os, {
    required this.debug,
    required this.enableAsserts,
    this.throws = false,
  });

  String postfix() {
    String result = '.${os.name}';
    if (debug) {
      result += '.debug';
    }
    if (enableAsserts) {
      result += '.withAsserts';
    }
    return result;
  }
}

class TestOptions {
  static const Option<bool?> debug = Option('--debug', BoolValue(null));

  static const Option<bool?> enableAsserts = Option(
    '--enable-asserts',
    BoolValue(null),
  );

  static const Option<String?> targetOS = Option('--target-os', StringValue());

  static const List<Option> options = [debug, enableAsserts, targetOS];
}

runTestCase(Uri source, TestCase testCase) async {
  final target = new VmTarget(new TargetFlags());
  Component component = await compileTestCaseToKernelProgram(
    source,
    target: target,
    environmentDefines: {
      'test.define.debug': testCase.debug ? 'true' : 'false',
      'test.define.enableAsserts': testCase.enableAsserts ? 'true' : 'false',
    },
  );

  final reporter = TestErrorReporter();
  final evaluator = VMConstantEvaluator.create(
    target,
    component,
    testCase.os,
    enableAsserts: testCase.enableAsserts,
    errorReporter: reporter,
  );
  late String actual;
  if (testCase.throws) {
    try {
      component = transformComponent(
        target,
        component,
        evaluator,
        testCase.enableAsserts,
      );
      final kernel = kernelLibraryToString(
        component.mainMethod!.enclosingLibrary,
      );
      fail("Expected compilation failure, got:\n$kernel");
    } on PlatformConstError catch (e) {
      final buffer = StringBuffer();
      for (final message in reporter.reportedMessages) {
        buffer.writeln(message);
      }
      buffer
        ..write('Member: ')
        ..writeln(e.member.name);
      final uri = e.uri;
      if (uri != null) {
        buffer
          ..write('File: ')
          ..writeln(uri.pathSegments.last);
      }
      if (e.offset >= 0) {
        buffer
          ..write('Offset: ')
          ..writeln(e.offset);
      }
      buffer
        ..write('Message: ')
        ..writeln(e.message);
      actual = buffer.toString();
    }
  } else {
    component = transformComponent(
      target,
      component,
      evaluator,
      testCase.enableAsserts,
    );
    if (reporter.reportedMessages.isNotEmpty) {
      fail('Expected no errors, got:\n${reporter.reportedMessages.join('\n')}');
    }
    verifyComponent(
      target,
      VerificationStage.afterGlobalTransformations,
      component,
    );
    actual = kernelLibraryToString(component.mainMethod!.enclosingLibrary);
  }
  compareResultWithExpectationsFile(
    source,
    actual,
    expectFilePostfix: testCase.postfix(),
  );
}

void runWithTargetOS(ParsedOptions? parsedOptions, void Function(TargetOS) fn) {
  TargetOS? specified;
  if (parsedOptions != null) {
    final s = TestOptions.targetOS.read(parsedOptions);
    if (s != null) {
      specified = TargetOS.fromString(s);
      if (specified == null) {
        fail('Failure parsing options: unknown target OS $s');
      }
    }
  }
  if (specified != null) {
    fn(specified);
  } else {
    for (final targetOS in TargetOS.values) {
      fn(targetOS);
    }
  }
}

void runWithBool(
  Option<bool?> option,
  ParsedOptions? parsedOptions,
  void Function(bool) fn,
) {
  bool? specified;
  if (parsedOptions != null) {
    specified = option.read(parsedOptions);
  }
  if (specified != null) {
    fn(specified);
  } else {
    for (final value in [true, false]) {
      fn(value);
    }
  }
}

void runTest(String path, Uri uri, {bool throws = false}) {
  ParsedOptions? options;
  final optionsFile = File('$path.options');
  if (optionsFile.existsSync()) {
    options = ParsedOptions.parse(
      ParsedOptions.readOptionsFile(optionsFile.readAsStringSync()),
      TestOptions.options,
    );
  }

  runWithTargetOS(options, (os) {
    runWithBool(TestOptions.enableAsserts, options, (enableAsserts) {
      runWithBool(TestOptions.debug, options, (debug) {
        final testCase = TestCase(
          os,
          debug: debug,
          enableAsserts: enableAsserts,
          throws: throws,
        );
        test('$path${testCase.postfix()}', () => runTestCase(uri, testCase));
      });
    });
  });
}

main() {
  group('platform-use-transformation', () {
    final testCasesPath = path.join(
      pkgVmDir,
      'testcases',
      'transformations',
      'vm_constant_evaluator',
    );

    group('successes', () {
      final successCasesPath = path.join(testCasesPath, 'successes');
      for (var entry
          in Directory(
            successCasesPath,
          ).listSync(recursive: true, followLinks: false).reversed) {
        if (entry.path.endsWith('.dart')) {
          runTest(entry.path, entry.uri);
        }
      }
    });

    group('failures', () {
      final errorCasesPath = path.join(testCasesPath, 'errors');

      for (var entry
          in Directory(
            errorCasesPath,
          ).listSync(recursive: true, followLinks: false).reversed) {
        if (entry.path.endsWith('.dart')) {
          runTest(entry.path, entry.uri, throws: true);
        }
      }
    });
  });
}
